package com.googlecode.mvcguice.routing;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.InvocationHandler;

import org.apache.commons.lang.StringUtils;

import com.google.common.primitives.Primitives;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Provider;
import com.google.inject.Scopes;
import com.google.inject.internal.UniqueAnnotations;
import com.googlecode.mvcguice.ControllerAction;
import com.googlecode.mvcguice.ControllerDispatcher;
import com.googlecode.mvcguice.Method;
import com.googlecode.mvcguice.action.Action;
import com.googlecode.mvcguice.routing.RoutesModule.ControllerBindingBuilder;
import com.googlecode.mvcguice.routing.RoutesModule.MethodBindingBuilder;
import com.googlecode.mvcguice.util.Exceptions;
import com.googlecode.mvcguice.util.Types;

class RoutesModuleBuilder extends AbstractModule {
	private boolean routeInProgress = false;
	private boolean allowCapturingBinders = false;
	private final List<ParameterBinder> capturedBinders = new ArrayList<ParameterBinder>();
	private final List<RouteBindingBuilder> builders = new ArrayList<RouteBindingBuilder>();
	private UrlPattern currUrlPattern;

	@Override
	protected void configure() {
		// TODO: add validation for same patterns

		Set<Class<?>> controllers = new HashSet<Class<?>>();
		for (final RouteBindingBuilder builder : builders) {
			controllers.add(builder.controller);
		}
		for (Class<?> controller : controllers) {
			bind(controller).in(Scopes.SINGLETON);
		}
		//TODO: add forward support on routing level
		//TODO: add urls caching support on routing level
		final ReverseRouter.Builder reverseRouterBuilder = new ReverseRouter.Builder();
		for (final RouteBindingBuilder builder : builders) {
			final List<ParameterBinder> parameterBinders = builder.binders;
			final java.lang.reflect.Method method = builder.controllerMethod;
			bind(Key.get(ControllerDispatcher.class, UniqueAnnotations.create())).toProvider(
					new Provider<ControllerDispatcher>() {
						@Inject
						Injector injector;

						@Override
						public ControllerDispatcher get() {
							final Object instance = injector.getInstance(builder.controller);
							ControllerAction action = new ControllerAction(instance, method, parameterBinders);
							return new ControllerDispatcher(builder.urlPattern, builder.methods, action);
						}
					}).in(Scopes.SINGLETON);

			final UrlGenerator urlGenerator = new UrlGenerator(builder.urlPattern, parameterBinders);
			reverseRouterBuilder.addRule(builder.controller, builder.controllerMethod, urlGenerator);
		}
		bind(ReverseRouter.class).toInstance(reverseRouterBuilder.build());
	}

	public MethodBindingBuilder serve(String urlPattern) {
		if (urlPattern == null) {
			String msg = "URL pattern must be not null.";
			throw configurationException(msg);
		}
		if (routeInProgress) {
			String msg = "Previous route is incomplete.";
			throw configurationException(msg);
		}
		if (!UrlPattern.isValid(urlPattern)) {
			String msg = "Invalid URL pattern";
			throw configurationException(msg);
		}

		routeInProgress = true;

		currUrlPattern = new UrlPattern(urlPattern);
		return new RouteBindingBuilder(currUrlPattern);
	}

	private RoutingConfigurationException configurationException(String msg) {
		return Exceptions.cutStackTrace(new RoutingConfigurationException(msg), 3);
	}

	public void captureVar(Class<?> type, String name) {
		if (name == null) {
			throw configurationException("Variable's value template must be not null.");
		}
		final ParameterBinder binder = ParameterBinder.forVariable(type, name);
		if (!currUrlPattern.getVariables().contains(name)) {
			throw configurationException("Variable " + binder + " is not available in URL pattern");
		}
		capture(binder);
	}

	public void captureSimpleParam(Class<?> type, String name) {
		if (StringUtils.isBlank(name)) {
			throw configurationException("Parameter's name must be not blank.");
		}
		capture(ParameterBinder.forSimpleParam(type, name));
	}

	public void captureSimpleParam(Class<?> type, String name, Object defaultValue) {
		if (StringUtils.isBlank(name)) {
			throw configurationException("Parameter's name must be not blank.");
		}
		if (defaultValue == null) {
			throw configurationException("Parameter's default value must be not null.");
		}
		capture(ParameterBinder.forSimpleParam(type, name, defaultValue));
	}

	public void captureComplexParam(Class<?> type, String name) {
		if (StringUtils.isBlank(name)) {
			throw configurationException("Parameter's name must be not blank.");
		}
		if (!Types.isInstantiateable(type)) {
			throw configurationException("Interfaces or abstract classes are not allowed");
		}
		if (Types.isSimpleType(type)) {
			throw configurationException("Simple types are not allowed");
		}
		capture(ParameterBinder.forComplexParam(type, name));
	}

	public void captureConstant(Class<?> type, Object value) {
		if (value == null) {
			throw configurationException("Constant value must be not null.");
		}
		capture(ParameterBinder.forConstant(type, value));
	}

	public void captureRequest() {
		capture(ParameterBinder.forRequest());
	}

	public void captureResponse() {
		capture(ParameterBinder.forResponse());
	}

	private void capture(ParameterBinder binder) {
		if (!allowCapturingBinders) {
			String msg = "Unexpected capturing binder: '" + binder + "' called before with.";
			throw Exceptions.cutStackTrace(new RoutingConfigurationException(msg), 3);
		}
		capturedBinders.add(binder);
	}

	private final class RouteBindingBuilder implements RoutesModule.MethodBindingBuilder {
		private final UrlPattern urlPattern;
		private Class<?> controller;
		private java.lang.reflect.Method controllerMethod;
		private Set<Method> methods = EnumSet.allOf(Method.class);
		private List<ParameterBinder> binders;

		private RouteBindingBuilder(UrlPattern urlPattern) {
			this.urlPattern = urlPattern;
		}

		@Override
		public <T> T with(Class<T> controller) {
			if (controller == null) {
				throw configurationException("Parameter 'controller' must be not null.");
			}
			if (!Types.isInstantiateable(controller)) {
				throw configurationException("Interfaces or abstract controllers are not allowed in 'with'");
			}

			this.controller = controller;
			allowCapturingBinders = true;
			return capturingProxy(controller);
		}

		@Override
		public ControllerBindingBuilder method(Method method) {
			if (method == null) {
				throw configurationException("Parameter 'method' must be not null.");
			}
			methods = EnumSet.of(method);
			return this;
		}

		@Override
		public ControllerBindingBuilder methods(Method method, Method... methods) {
			if (method == null) {
				// TODO: add check for other args
				throw configurationException("Parameter 'method' must be not null.");
			}
			this.methods = EnumSet.of(method, methods);
			return this;
		}

		@Override
		public ControllerBindingBuilder methods(Set<Method> methods) {
			if (methods == null) {
				throw configurationException("Parameter 'methods' must be not null.");
			}
			this.methods = EnumSet.copyOf(methods);
			return this;
		}

		private <T> T capturingProxy(Class<T> clazz) {
			final Enhancer enhancer = new Enhancer();
			enhancer.setUseFactory(false);
			enhancer.setSuperclass(clazz);
			enhancer.setCallback(new CapturingMethodInvocation());

			@SuppressWarnings("unchecked")
			final T instance = (T) enhancer.create();
			return instance;
		}

		private RoutingConfigurationException configurationException(String msg) {
			return Exceptions.cutStackTrace(new RoutingConfigurationException(msg), 2);
		}

		private final class CapturingMethodInvocation implements InvocationHandler {
			@Override
			public Object invoke(Object proxy, java.lang.reflect.Method method, Object[] args) throws Throwable {
				if ("finalize".equals(method.getName())) {
					return null;
				}
				if (method.getReturnType() != Action.class) {
					String msg = "Wrong return type for method: '" + method + "'" +
							" expected: " + Action.class.getName() +
							", actual: " + method.getReturnType();
					throw configurationException(msg);
				}
				final Class<?>[] methodParameterTypes = method.getParameterTypes();

				try {
					if (methodParameterTypes.length != capturedBinders.size()) {
						String msg = "Wrong number of parameter bindings. For method: '" + method + "'" +
								" expected: " + methodParameterTypes.length +
								", actual binded: " + capturedBinders.size();
						throw configurationException(msg);
					}

					for (int i = 0, n = methodParameterTypes.length; i < n; i++) {
						Class<?> methodParameterType = methodParameterTypes[i];
						Class<?> capturedbindingType = capturedBinders.get(i).type;
						if (!isSameParameterType(capturedbindingType, methodParameterType)) {
							String msg = "Wrong binder type. For method: '" + method + "'" +
									" parameter: " + (i + 1) +
									". Expected type: " + methodParameterType +
									", actual type: " + capturedBinders.get(i).type;
							throw configurationException(msg);
						}
					}
				} finally {
					binders = new ArrayList<ParameterBinder>(capturedBinders);
					capturedBinders.clear();
					routeInProgress = false;
					allowCapturingBinders = false;
					currUrlPattern = null;
				}

				controllerMethod = method;
				builders.add(RouteBindingBuilder.this);

				return null;
			}

			private boolean isSameParameterType(Class<?> capturedParameterType, Class<?> methodParameterType) {
				if (methodParameterType.isPrimitive()) {
					return Primitives.wrap(methodParameterType) == capturedParameterType;
				} else {
					return methodParameterType.isAssignableFrom(capturedParameterType);
				}
			}

			/*
			 * This method removes two top stack trace elements: invocation's handler and actual proxy method.
			 * This gives direct point to line where binding was performed.
			 */
			private RoutingConfigurationException configurationException(String msg) {
				return Exceptions.cutStackTrace(new RoutingConfigurationException(msg), 3);
			}
		}
	}
}
